using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Xml;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.IdentityModel;
using System.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.IdentityModel.Selectors;
using System.IdentityModel.Policy;
using System.IdentityModel.Claims;
using System.Runtime.Remoting;
using System.Security.Principal;
using System.Configuration;
using System.ServiceModel.Security.Tokens;
using System.Web;

namespace OpenIDTokenProcessor
{
    public class OpenIDToken
    {
		private static System.Collections.Hashtable _certificates = new System.Collections.Hashtable();
		private XmlDocument _token = new XmlDocument();
		private IDictionary<string, string> _openIdFields = new Dictionary<string, string>();
		private string _rawOpenIdFields = String.Empty;

		public string RawOpenIdFields
		{
			get { return _rawOpenIdFields; }
		}

		public IDictionary<string, string> OpenIdFields
		{
			get { return _openIdFields; }
		}

        #region Token Decryption Constants
        private static int[] TripleDes =  { "http://www.w3.org/2001/04/xmlenc#tripledes-cbc".GetHashCode(), "http://www.w3.org/2001/04/xmlenc#kw-tripledes".GetHashCode() };
        private static int[] Aes = { "http://www.w3.org/2001/04/xmlenc#aes128-cbc".GetHashCode(), "http://www.w3.org/2001/04/xmlenc#aes192-cbc".GetHashCode(), "http://www.w3.org/2001/04/xmlenc#aes256-cbc".GetHashCode(), "http://www.w3.org/2001/04/xmlenc#kw-aes128".GetHashCode(), "http://www.w3.org/2001/04/xmlenc#kw-aes192".GetHashCode(), "http://www.w3.org/2001/04/xmlenc#kw-aes256".GetHashCode() };

        static class XmlEncryptionStrings
        {
            public const string Namespace = "http://www.w3.org/2001/04/xmlenc#";
            public const string EncryptionMethod = "EncryptionMethod";
            public const string CipherValue = "CipherValue";
            public const string Encoding = "Encoding";
            public const string MimeType = "MimeType";
            public const string Algorithm = "Algorithm";
        }

        static class XmlSignatureStrings
        {
            public const string Namespace = "http://www.w3.org/2000/09/xmldsig#";
            public const string KeyInfo = "KeyInfo";
            public const string DigestMethod = "DigestMethod";
        }

        static class WSSecurityStrings
        {
            public const string Namespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
            public const string KeyIdentifier = "KeyIdentifier";
        }
        #endregion

        /// <summary>
        /// Token Constructor
        /// </summary>
        /// <param name="xmlToken">Encrypted xml token</param>
		public OpenIDToken(String xmlToken)
        {
            byte[] decryptedData = decryptToken(xmlToken);

            XmlReader reader = new XmlTextReader(new StreamReader(new MemoryStream(decryptedData), Encoding.UTF8));
			_token.Load(reader);
			ParseOpenID();
        }

		/// <summary>
		/// OpenID protocol parser
		/// </summary>
		private void ParseOpenID()
		{
			_rawOpenIdFields = _token.InnerText;
			string key = "", value = "";
			bool getvalue = false;
			for (int i = 1, x = 1; i < _rawOpenIdFields.Length; ++i)
			{
				if (!getvalue && _rawOpenIdFields[i] == ':')
				{
					key = _rawOpenIdFields.Substring(x, i - x);
					x = i + 1;
					getvalue = true;
				}
				else if (_rawOpenIdFields[i] == '\n')
				{
					value = _rawOpenIdFields.Substring(x, i - x);
					x = i + 1;
					getvalue = false;
					_openIdFields.Add(key, value);
				}
			}
		}

		/// <summary>
		/// Decrpyts a security token from an XML EncryptedData 
		/// </summary>
		/// <param name="xmlToken">the XML token to decrypt</param>
		/// <returns>A byte array of the contents of the encrypted token</returns>
		private static byte[] decryptToken(string xmlToken)
        {
            int encryptionAlgorithm;
            int m_keyEncryptionAlgorithm;
            string m_keyHashAlgorithm;
            byte[] thumbprint;
            byte[] securityTokenData;
            byte[] symmetricKeyData;

            XmlReader reader = new XmlTextReader(new StringReader(xmlToken));

            // Find the EncryptionMethod element, grab the Algorithm
            if (!reader.ReadToDescendant(XmlEncryptionStrings.EncryptionMethod, XmlEncryptionStrings.Namespace))
                throw new ArgumentException("Cannot find token EncryptedMethod.");
            encryptionAlgorithm = reader.GetAttribute(XmlEncryptionStrings.Algorithm).GetHashCode();

            // Find the EncryptionMethod element for the key, grab the Algorithm
            if (!reader.ReadToFollowing(XmlEncryptionStrings.EncryptionMethod, XmlEncryptionStrings.Namespace))
                throw new ArgumentException("Cannot find key EncryptedMethod.");
            m_keyEncryptionAlgorithm = reader.GetAttribute(XmlEncryptionStrings.Algorithm).GetHashCode();

            // Find the Digest method for the key identifier
            if (!reader.ReadToFollowing(XmlSignatureStrings.DigestMethod, XmlSignatureStrings.Namespace))
                throw new ArgumentException("Cannot find Digest Method.");
            m_keyHashAlgorithm = reader.GetAttribute(XmlEncryptionStrings.Algorithm);
            // Find the key identifier
            if (!reader.ReadToFollowing(WSSecurityStrings.KeyIdentifier, WSSecurityStrings.Namespace))
                throw new ArgumentException("Cannot find Key Identifier.");
            reader.Read();
            thumbprint = Convert.FromBase64String(reader.ReadContentAsString());

            // Find the encrypted Symmetric Key
            if (!reader.ReadToFollowing(XmlEncryptionStrings.CipherValue, XmlEncryptionStrings.Namespace))
                throw new ArgumentException("Cannot find symmetric key.");
            reader.Read();
            symmetricKeyData = Convert.FromBase64String(reader.ReadContentAsString());

            // Find the encrypted Security Token
            if (!reader.ReadToFollowing(XmlEncryptionStrings.CipherValue, XmlEncryptionStrings.Namespace))
                throw new ArgumentException("Cannot find encrypted security token.");
            reader.Read();
            securityTokenData = Convert.FromBase64String(reader.ReadContentAsString());

            reader.Close();

            SymmetricAlgorithm alg = null;
            X509Certificate2 certificate = FindCertificate(thumbprint);

            foreach (int i in Aes)
                if (encryptionAlgorithm == i)
                {
                    alg = new RijndaelManaged();
                    break;
                }
            if (null == alg)
                foreach (int i in TripleDes)
                    if (encryptionAlgorithm == i)
                    {
                        alg = new TripleDESCryptoServiceProvider();
                        break;
                    }

            if (null == alg)
                throw new ArgumentException("Could not determine Symmetric Algorithm");

			alg.Key = (certificate.PrivateKey as RSACryptoServiceProvider).Decrypt(symmetricKeyData, true); ;
            int ivSize = alg.BlockSize / 8;
            byte[] iv = new byte[ivSize];
            Buffer.BlockCopy(securityTokenData, 0, iv, 0, iv.Length);
            alg.Padding = PaddingMode.ISO10126;
            alg.Mode = CipherMode.CBC;
            ICryptoTransform decrTransform = alg.CreateDecryptor(alg.Key, iv);
            byte[] plainText = decrTransform.TransformFinalBlock(securityTokenData, iv.Length, securityTokenData.Length - iv.Length);
            decrTransform.Dispose();
            return plainText;
        }

        /// <summary>
        /// Finds certificates by thumbprint, caches them statically.
        /// 
        /// This may be configured with the following parameters in the web.config file:
        /// 
        /// The Certificate Thumbprint can be hard coded:
        ///   <add name="CertifcateThumbprint" value="01234567890ABCDEFEDCBA01234567890ABCDEFEDCBA" />
        ///
        /// The Store Name Can be specified:
        ///   <add name="StoreName" value="My" />  
        ///     
        /// The Store Location Can be specified
        ///   <add name="StoreLocation" value="LocalMachine" />
        /// 
        /// </summary>
        /// <param name="thumbprint">Certificate's thumbprint</param>
        /// <returns>the certificate, or the default certificate</returns>
        private static X509Certificate2 FindCertificate(object thumbprint)
        {
            X509Certificate2 result = _certificates[thumbprint] as X509Certificate2;

            if (null == result)
            {
                StoreName storeName = StoreName.My;
                StoreLocation storeLocation = StoreLocation.LocalMachine;

                string rpCertificateThumbprint = System.Configuration.ConfigurationManager.AppSettings["CertificateThumbprint"];
                string rpStoreName = ConfigurationManager.AppSettings["StoreName"];
                string rpStoreLocation = ConfigurationManager.AppSettings["StoreLocation"];

                if (!string.IsNullOrEmpty(rpStoreName))
                    storeName = (StoreName)Enum.Parse(typeof(StoreName), rpStoreName, true);

                if (!string.IsNullOrEmpty(rpStoreLocation))
                    storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), rpStoreLocation, true);

                X509Store store = new X509Store(storeName, storeLocation);
                store.Open(OpenFlags.ReadOnly);

                // if the thumbprint is passed as a string thumbprint, good
                // otherwise if it is a byte array turn it to a string
                string strThumbprint = "";
                if (null != thumbprint as byte[])
                    foreach (byte b in (thumbprint as byte[]))
                        strThumbprint += b.ToString("X2");
                else
                    strThumbprint = thumbprint.ToString();

                // search based off the thumbprint in the token.
                X509Certificate2Collection collection = store.Certificates.Find(X509FindType.FindByThumbprint, strThumbprint, true);
                if (collection.Count == 0)
                {
                    // if it's not found there, search by the thumbprint specified in the App.config
                    if (string.IsNullOrEmpty(rpCertificateThumbprint))
                        throw new Exception("Relying Party Certificate thumbprint not specified");

                    collection = store.Certificates.Find(X509FindType.FindByThumbprint, rpCertificateThumbprint, true);
                    if (collection.Count == 0)
                        throw new Exception("Unable to find certificate by thumbprint.");
                }
                result = collection[0];
                _certificates.Add(thumbprint, result);
            }
            return result;
        }
    }
}
